一般用 SeekBar 除了 google 自己那一票 app 以及系统,开发者几乎都不会直接用原来的 SeekBar 的图的,都要自己换图。这里就所说 SeekBar 换图的事。
结构
首先来看看 SeekBar 的构成:
SeekBar 由一个进度条和上面的滑块构成。一般换图就换这2个东西就 OK 了。一般是要准备3张图片:进度条2张,一张背景(background)、一张滑动的进度(progress)、一张是滑块(thumb)。
拿个例子说事, UI 说界面的亮度调节的视觉是这样的:
然后给了3张图,一般换图么, SeekBar 继承关系是: SeekBar —-> AbsSeekBar —> ProgressBar 。 换个 progressDrawable(ProgressBar的) 和 thumb(AbsSeekBar的) 就行了。
1 2 3 4 5 6 7 8
| <SeekBar android:layout_width="300dp" android:layout_height="wrap_content" android:layout_centerInParent="true" android:progressDrawable="@drawable/seek_drawable" android:thumb="@drawable/seek_thumb" />
|
thumb 的 drawable 直接是滑块的 png 就行。progressDrawable 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@android:id/background" android:drawable="@drawable/seek_progress_bk" > </item> <item android:id="@android:id/progress" > <clip android:drawable="@drawable/seek_progress" > </clip> </item> </layer-list>
|
progressDrawable 可以用 LayerDrawable 同时指定进度条的背景(background)和进度(progress),progress 的 clip 的意思是说:这个要用这个图片的一部门来表示进度,而不是用图片的全部来表示,想想看进度条的表示方式,就是这样的啦。这个 clip 还可以设置方向,进度条可以是水平的也可以是竖直的。
然后坑就来了。一般 SeekBar 的滑块都会比进度条大,这里 UI 给的是 25x29 ,那个进度是 9-patch 的,20x6。我原来以为 SeekBar 的大小应该是 300 x 29,滑块是 25x29,然后那个进度条高度就是 6,居中显示。但是出来的结果确是滑块的大小确实 25x29,但是进度条却是 13 的高度(居中倒是居中),感觉肥了不少。
刚开始以为 ProgressBar 可以设啥 padding 之类的东西,但是不找到,然后尝试把进度体的图改称和 thumb 的一样高(png 的用透明填充),但是发现还是达不到预期效果。无奈只好去翻代码了。
坑爹的设计
先是在 ProgressBar.java 中发现 progressDrawable 设置的地方:
1 2 3 4 5 6 7 8
| Drawable drawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable); if (drawable != null) { drawable = tileify(drawable, false); setProgressDrawable(drawable); }
|
这段在 ProgressBar 的构造函数中,发现 xml 里指定的 progressDrawable 其实调用 setProgressDrawable 来设置的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public void setProgressDrawable(Drawable d) { boolean needUpdate; if (mProgressDrawable != null && d != mProgressDrawable) { mProgressDrawable.setCallback(null); needUpdate = true; } else { needUpdate = false; } if (d != null) { d.setCallback(this); if (canResolveLayoutDirection()) { d.setLayoutDirection(getLayoutDirection()); } int drawableHeight = d.getMinimumHeight(); if (mMaxHeight < drawableHeight) { mMaxHeight = drawableHeight; requestLayout(); } } mProgressDrawable = d; if (!mIndeterminate) { mCurrentDrawable = d; postInvalidate(); } if (needUpdate) { updateDrawableBounds(getWidth(), getHeight()); updateDrawableState(); doRefreshProgress(R.id.progress, mProgress, false, false); doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false); } }
|
然后 setProgressDrawable 是把设置的 progressDrawable 保存在了 mProgressDrawable 的成员变量中。然后通过搜索发现根据 ProgressBar 的样式 mProgressDrawable 为被选为 mCurrentDrawable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public synchronized void setIndeterminate(boolean indeterminate) { if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) { mIndeterminate = indeterminate; if (indeterminate) { mCurrentDrawable = mIndeterminateDrawable; startAnimation(); } else { mCurrentDrawable = mProgressDrawable; stopAnimation(); } } }
|
然后去 onDraw 里面看看,原来 mCurrentDrawable 就是画进度条的东西:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Override protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); Drawable d = mCurrentDrawable; if (d != null) { canvas.save(); if(isLayoutRtl()) { canvas.translate(getWidth() - mPaddingRight, mPaddingTop); canvas.scale(-1.0f, 1.0f); } else { canvas.translate(mPaddingLeft, mPaddingTop); } long time = getDrawingTime(); if (mHasAnimation) { mAnimation.getTransformation(time, mTransformation); float scale = mTransformation.getAlpha(); try { mInDrawing = true; d.setLevel((int) (scale * MAX_LEVEL)); } finally { mInDrawing = false; } postInvalidateOnAnimation(); } d.draw(canvas); canvas.restore(); if (mShouldStartAnimationDrawable && d instanceof Animatable) { ((Animatable) d).start(); mShouldStartAnimationDrawable = false; } } }
|
onDraw 里面是直接使用 drawable 的 draw 方法来绘制的,那说明肯定是在哪个地方使用 setBounds 来设置 drawable 的 bound 来设置绘制位置的(这里称赞一下 android 的 drawable 家族的设计,真的很棒,抽象的很好,功能也强大,背景不管是 bitmap、9-patch、color、gradient、selector、layer 都只要 draw 就能画出来,只要是 drawable 都能够当作背景参数传入)。
然后搜一下 setBrounds 的地方,发现并不多,然后和 mProgressDrawable 相关的 setBounds 的地方就一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| private void updateDrawableBounds(int w, int h) { w -= mPaddingRight + mPaddingLeft; h -= mPaddingTop + mPaddingBottom; int right = w; int bottom = h; int top = 0; int left = 0; if (mIndeterminateDrawable != null) { if (mOnlyIndeterminate && !(mIndeterminateDrawable instanceof AnimationDrawable)) { final int intrinsicWidth = mIndeterminateDrawable.getIntrinsicWidth(); final int intrinsicHeight = mIndeterminateDrawable.getIntrinsicHeight(); final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight; final float boundAspect = (float) w / h; if (intrinsicAspect != boundAspect) { if (boundAspect > intrinsicAspect) { final int width = (int) (h * intrinsicAspect); left = (w - width) / 2; right = left + width; } else { final int height = (int) (w * (1 / intrinsicAspect)); top = (h - height) / 2; bottom = top + height; } } } if (isLayoutRtl()) { int tempLeft = left; left = w - right; right = w - tempLeft; } mIndeterminateDrawable.setBounds(left, top, right, bottom); } if (mProgressDrawable != null) { mProgressDrawable.setBounds(0, 0, right, bottom); } }
|
加点打印看看设置的 bound 是多少(编译出来的是 frameworks.jar),发现是 (0,0,268,29),哎,这里的是 29 的高度啊。在 onDraw 再加点打印(把 drawable 的 getBounds() 打印出来),发现是 (0, 8, 268, 21) ,这里就和显示上的对上了,高度 13 ,肥了很多,那应该在别的地方有设置。
分别去 SeekBar、AbsSeekBar 里搜索下 setBounds,发现 SeekBar 里没有, AbsSeekBar 里有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| private void updateThumbPos(int w, int h) { Drawable d = getCurrentDrawable(); Drawable thumb = mThumb; int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); int max = getMax(); float scale = max > 0 ? (float) getProgress() / (float) max : 0; if (thumbHeight > trackHeight) { if (thumb != null) { setThumbPos(w, thumb, scale, 0); } int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; if (d != null) { d.setBounds(0, gapForCenteringTrack, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack - mPaddingTop); } } else { if (d != null) { d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - mPaddingTop); } int gap = (trackHeight - thumbHeight) / 2; if (thumb != null) { setThumbPos(w, thumb, scale, gap); } } }
|
哎,好多加、减的计算咧,分析上面的代码会发现是这样的: 如果 thumbHeight > trackHeight 的,那么进度条的 bounds 其实是居中。thumbHeight 的是由设置的 thumb 的 drawable 的 IntrinsicHeight 来决定的(一般就是图片的高度)。主要在于 trackHeight 是多少, 打印出来发现是 13 :
int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom);
打印出来发现,padding 是 0,高度 h 是 29,这个高度看 AbsSeekBar 的 onMeasure 函数可以知道如果没有明确指定 view 的高度(使用 wrap_content)的话,由 thumb 和 currentDrawable(progressDrawable) 中 IntrinsicHeight 最大(加上 padding )的决定。这里 thumb 的 29 明显比 progressDrawable 的 6 要大,所以主要就看 mMaxHeight 了, 这个 mMaxHeight 在 AbsSeekBar 中没有定义,那就是父类 ProgressBar 的,跑去一看发现在构造函数中:
1 2
| mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
|
这个东西由 xml 中的 android:maxHeight 决定的,如果不指定,则使用系统 style 中的默认值,这里好像是 13。果然坑爹咧,进度条的高度系统不是用你图片的高度,而是使用 android:maxHeight 来决定(大多数情况下由这个决定,因为很前面的代码发现,这个需要 thumb 图片的高度比进度条的图片高才行,不过一般都是这样的)。
所以我设了下 maxHeight 为 6 (进度条图片高度),果然进度条就不肥了,高度老老实实变成 6 了,官方文档啥都没说。
1 2 3 4 5 6 7 8 9
| <SeekBar android:layout_width="300dp" android:layout_height="wrap_content" android:layout_centerInParent="true" android:progressDrawable="@drawable/seek_drawable" android:thumb="@drawable/seek_thumb" android:maxHeight="6dp" />
|
果然不肥了: